package org.iblink.gui.swing.manager.impl;

import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;

import javax.swing.DropMode;
import javax.swing.JComponent;
import javax.swing.JOptionPane;
import javax.swing.TransferHandler;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.ExpandVetoException;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

import org.iblink.core.domain.model.Asset;
import org.iblink.core.domain.model.Category;
import org.iblink.core.service.CategoryService;
import org.iblink.gui.swing.actions.listener.CategorySelectionListener;
import org.iblink.gui.swing.actions.listener.FileImportListener;
import org.iblink.gui.swing.dnd.flavor.AssetListFlavor;
import org.iblink.gui.swing.dnd.flavor.CategoryFlavor;
import org.iblink.gui.swing.manager.CategoryTreeManager;
import org.iblink.gui.swing.views.CategoryTree;
import org.iblink.gui.swing.views.component.CategoryNode;
import org.iblink.gui.swing.views.component.CategoryPopupMenu;

/**
 * {@link org.iblink.gui.swing.manager.CategoryTreeManager} implementation for managing
 * user actions upon a {@link org.iblink.gui.swing.views.CategoryTree}.
 * 
 * @author tpares
 *
 */
public class CategoryTreeManagerImpl extends TransferHandler implements CategoryTreeManager {
  /**
   * Generated serial version UID
   */
  private static final long serialVersionUID = 8422668830590616283L;

  /**
   * Category service to manage categories
   */
  private CategoryService categoryService;
  
  /**
   * The {@code CategoryTree} to manage
   */
  private CategoryTree categoryTree;
  
  private List<CategorySelectionListener> selectionListeners;
  
  private List<FileImportListener> fileImportListeners;
  
  private CategoryPopupMenu categoryPopupMenu;
  
  @Override
  public void initTree(CategoryTree tree) {
    // add listeners
    tree.getModel().addTreeModelListener(this);
    tree.addTreeSelectionListener(this);
    tree.addMouseListener(this);
    tree.addTreeWillExpandListener(this);
    tree.addKeyListener(this);
    
    // set properties
    tree.setEditable(true);
    tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
    tree.setShowsRootHandles(true);
    
    // Drag and drop
    tree.setDragEnabled(true);
    tree.setTransferHandler(this);
    tree.setDropMode(DropMode.ON);
    
    // keep tree instance
    categoryTree = tree;
    
    // get root instance (used to build the tree)
    CategoryNode top = (CategoryNode) tree.getModel().getRoot();
    
    // get root categories in DB
    CategoryNode node;
    for (final Iterator<Category> iter = categoryService.getRoots().iterator();iter.hasNext();) {
      Category category = iter.next();
      // also lazily build sub nodes to know whether or not the current node has children
      node = buildNode(category);
      top.add(node);
    }
    // Expand root to show "real" categories
    tree.expandRow(0);
  }

  @Override
  public void treeWillCollapse(TreeExpansionEvent event)
      throws ExpandVetoException {
    
  }

  @SuppressWarnings("unchecked")
  @Override
  public void treeWillExpand(TreeExpansionEvent event)
      throws ExpandVetoException {
    final CategoryNode parentNode = (CategoryNode) event.getPath().getLastPathComponent();
    
    final Enumeration<CategoryNode> children = parentNode.children();
    while(children.hasMoreElements()) {
      CategoryNode node = (CategoryNode) children.nextElement();
      
      if (node.children().hasMoreElements()) {
        // node has already been loaded, skip it
        continue;
      }
      
      final Category category = node.getCategory();
      if (category != null) {
        for (final Iterator<Category> iter = categoryService.getSubCategories(category).iterator();iter.hasNext();) {
          Category nodeCategory = iter.next();
          // lazily build sub nodes to know whether or not the current node has children
          CategoryNode newNode = buildNode(nodeCategory);
          node.add(newNode);
        }
      }
    }
  }

  @Override
  public void treeNodesChanged(TreeModelEvent e) {
    // Parent information
    final CategoryNode parentNode = (CategoryNode) e.getTreePath().getLastPathComponent();
    
    // Changed Nodes
    for (int i = 0; i < e.getChildren().length; i++) {
      // Get changed node
      CategoryNode node = (CategoryNode)(e.getChildren()[i]);
      
      node.getCategory().setName(node.getUserObject().toString());
      categoryService.save(node.getCategory());
      
      ((DefaultTreeModel) categoryTree.getModel()).removeNodeFromParent(node);
      insertNodeIntoParent(node, parentNode);
    }
  }

  @Override
  public void treeNodesInserted(TreeModelEvent e) {

  }

  @Override
  public void treeNodesRemoved(TreeModelEvent e) {
    
  }
  
  /**
   * Recursively deletes a category.
   * 
   * @param category the category to delete
   */
  private void deleteCategory(Category category) {
    // delete sub categories
    List<Category> categories = categoryService.getSubCategories(category);
    for (final Iterator<Category> iter = categories.iterator(); iter.hasNext();) {
      Category cat = iter.next();
      // ask listeners before deletion
      for (Iterator<CategorySelectionListener> iterList = selectionListeners.iterator(); iterList.hasNext();) {
        if(!iterList.next().beforeCategoryDeleted(cat)) {
          return;
        }
      }
      deleteCategory(cat);
    }
    
    // ask listeners before deletion
    for (Iterator<CategorySelectionListener> iterList = selectionListeners.iterator(); iterList.hasNext();) {
      if(!iterList.next().beforeCategoryDeleted(category)) {
        return;
      }
    }
    // delete category
    categoryService.delete(category);
  }

  @Override
  public void treeStructureChanged(TreeModelEvent e) {

  }
  
  /**
   * Builds a node and its children, if exist.
   * <p>
   * Children nodes are lazily built for evident performance matter.
   * 
   * @param iCategory the category upon which this node is to be built
   * @return the {@link org.iblink.gui.swing.views.component.CategoryNode} built for category {@code iCategory}
   */
  private CategoryNode buildNode(Category iCategory) {
    // build a new node
    CategoryNode node = new CategoryNode(iCategory);
    
    // add its children
    for (final Iterator<Category> iter = categoryService.getSubCategories(iCategory).iterator();iter.hasNext();) {
      Category category = iter.next();
      node.add(new CategoryNode(category));
    }
    
    return node;
  }

  /**
   * Getter method for category service
   * 
   * @return the categoryService
   */
  public CategoryService getCategoryService() {
    return categoryService;
  }

  /**
   * Setter method for category service.
   * 
   * @param categoryService the categoryService to set
   */
  public void setCategoryService(CategoryService categoryService) {
    this.categoryService = categoryService;
  }

  @Override
  public void valueChanged(TreeSelectionEvent event) {
    if (event.getNewLeadSelectionPath() != null) {
      categoryPopupMenu.prepareForNode((CategoryNode)event.getNewLeadSelectionPath().getLastPathComponent());
      notifySelectionListeners(((CategoryNode)event.getNewLeadSelectionPath().getLastPathComponent()).getCategory());
    }
  }
  
  private void notifySelectionListeners(Category selected) {
    for (Iterator<CategorySelectionListener> iter = selectionListeners.iterator(); iter.hasNext();) {
      iter.next().categorySelected(selected);
    }
  }

  @Override
  public void mouseClicked(MouseEvent event) {
    
  }

  @Override
  public void mouseEntered(MouseEvent event) {
    
  }

  @Override
  public void mouseExited(MouseEvent event) {
    
  }

  @Override
  public void mousePressed(MouseEvent event) {
    
  }

  @Override
  public void mouseReleased(MouseEvent e) {
    if (e.isPopupTrigger()) {
      categoryTree.setSelectionPath(categoryTree.getPathForLocation(e.getX(), e.getY()));
      categoryPopupMenu.show(e.getComponent(),e.getX(), e.getY());
    }
  }

  @Override
  public void actionPerformed(ActionEvent e) {
    if (e.getActionCommand().equals(CategoryPopupMenu.ADDCHILD_ACTION)) {
      addEmptyChild(getSelectedNode());
    } else if (e.getActionCommand().equals(CategoryPopupMenu.ADDSIBLING_ACTION)) {
      addEmptyChild((CategoryNode)getSelectedNode().getParent());
    } else if (e.getActionCommand().equals(CategoryPopupMenu.RENAME_ACTION)) {
      TreePath newPath = new TreePath(getSelectedNode().getPath());
      categoryTree.setSelectionPath(newPath);
      categoryTree.scrollPathToVisible(newPath);
      categoryTree.startEditingAtPath(newPath);
    } else if (e.getActionCommand().equals(CategoryPopupMenu.DELETE_ACTION)) {
      handleDelete();
    }
  }
  
  /**
   * Handles the deletion of the currently selected category node.
   * <p>
   * This methods warns the user if sub-categories are also about to be deleted.
   */
  private void handleDelete() {
    int n = 1;
    CategoryNode parent = (CategoryNode)getSelectedNode().getParent();
    if (getSelectedNode().children().hasMoreElements()) {
      // node has children, should they be deleted as well?
      n = confirmRecursiveDeletion();
    }
    if (n != 0) {
      // delete category
      deleteCategory(getSelectedNode().getCategory());
      // remove node
      ((DefaultTreeModel) categoryTree.getModel()).removeNodeFromParent(getSelectedNode());
      // set parent node selected
      categoryTree.setSelectionPath(new TreePath(parent.getPath()));
    }
  }
  
  /**
   * Retrieves the currently selected node as a {@link org.iblink.gui.swing.views.component.CategoryNode}.
   * 
   * @return the currently selected {@code CategoryNode} instance.
   */
  private CategoryNode getSelectedNode() {
    return (CategoryNode) categoryTree.getSelectionPath().getLastPathComponent();
  }
  
  /**
   * Adds an empty child node to node {@code parent}.
   * 
   * @param parent the parent node to which a node has to be added.
   */
  private void addEmptyChild(CategoryNode parent) {
    // Create new Category
    Category category = new Category();
    category.setName("New");
    category.setParent(parent.getCategory());
    
    // persist new category
    categoryService.save(category);
    
    // Create new node from category
    CategoryNode node = new CategoryNode(category);
    
    // Insert new node
    ((DefaultTreeModel) categoryTree.getModel()).insertNodeInto(node, parent, 0);
    
    // ensure new node is visible, and edit its name
    TreePath newPath = new TreePath(node.getPath());
    categoryTree.setSelectionPath(newPath);
    categoryTree.scrollPathToVisible(newPath);
    categoryTree.startEditingAtPath(newPath);
  }
  
  /**
   * Shows a popup dialog box for the user to confirm sub-categories deletion.
   * 
   * @return user choice as integer: <ul><li>0 if action is cancelled,</li><li>1 if deletion is confirmed.</li></ul>
   */
  private int confirmRecursiveDeletion() {
    Object[] deleteOptions = {"Cancel", "Delete"};
    String deleteText = "This category has subcategories.\n"
                          + "Should they be deleted as well?";
    String deleteTitle = "Confirm deletion";
    
    return JOptionPane.showOptionDialog(categoryTree, deleteText, deleteTitle, JOptionPane.YES_NO_OPTION,
        JOptionPane.QUESTION_MESSAGE, null, deleteOptions, deleteOptions[1]);
  }

  @Override
  public void keyPressed(KeyEvent e) {
  }

  @Override
  public void keyReleased(KeyEvent e) {
    handleKey(e);
  }

  @Override
  public void keyTyped(KeyEvent e) {
  }
  
  /**
   * Handles keyboard events.
   * 
   * @param e the {@code KeyEvent} event to handle
   */
  private void handleKey(KeyEvent e) {
    if ((e.getKeyCode() == KeyEvent.VK_DELETE || e.getKeyCode() == KeyEvent.VK_BACK_SPACE) && getSelectedNode() != null) {
      handleDelete();
    }
  }
  
  public int getSourceActions(JComponent c) {
    //only allow move action
    return MOVE;
  }
  
  public Transferable createTransferable(JComponent c) {
    // move the node itself
    if (! (c instanceof CategoryTree)) {
      return null;
    }
    return getSelectedNode();
  }
  
  public void exportDone(JComponent c, Transferable t, int action) {
  }
  
  public boolean canImport(TransferSupport supp) {
    CategoryNode node = (CategoryNode)categoryTree.getClosestPathForLocation(supp.getDropLocation().getDropPoint().x, supp.getDropLocation().getDropPoint().y).getLastPathComponent();
    if (!node.isEditable()) {
      return false;
    }
    for (int i = 0; i < supp.getDataFlavors().length; i++) {
      if (supp.getDataFlavors()[i] instanceof CategoryFlavor) {
        try {
          CategoryNode t = (CategoryNode)supp.getTransferable().getTransferData(supp.getDataFlavors()[i]);
          // cannot move a node to one of its children
          return node.getSharedAncestor(t) != t;
        } catch (IOException e) {
          continue;
        } catch (UnsupportedFlavorException e) {
          continue;
        }
      } else if (supp.getDataFlavors()[i] instanceof AssetListFlavor) {
        return true;
      } else if (supp.getDataFlavors()[i].isFlavorJavaFileListType()) {
        return true;
      } 
    }
    
    return false;
  }
  
  private CategoryNode getTransferedNode(TransferSupport supp) {
    CategoryNode node = null;
    
    for (int i = 0; i < supp.getDataFlavors().length; i++) {
      if (supp.getDataFlavors()[i] instanceof CategoryFlavor) {
        try {
          return (CategoryNode)supp.getTransferable().getTransferData(supp.getDataFlavors()[i]);
        } catch (IOException e) {
          continue;
        } catch (UnsupportedFlavorException e) {
          continue;
        }
      }
    }
    
    return node;
  }
  
  @SuppressWarnings("unchecked")
  private List<File> getTransferedFiles(TransferSupport supp) {
    List<File> files = null;
    
    for (int i = 0; i < supp.getDataFlavors().length; i++) {
      if (supp.getDataFlavors()[i].isFlavorJavaFileListType()) {
        try {
          return (List<File>)supp.getTransferable().getTransferData(supp.getDataFlavors()[i]);
        } catch (IOException e) {
          continue;
        } catch (UnsupportedFlavorException e) {
          continue;
        }
      }
    }
    
    return files;
  }
  
  @SuppressWarnings("unchecked")
  private List<Asset> getTransferedAssetList(TransferSupport supp) {
    List<Asset> assets = null;
    
    for (int i = 0; i < supp.getDataFlavors().length; i++) {
      if (supp.getDataFlavors()[i] instanceof AssetListFlavor) {
        try {
          return (List<Asset>)supp.getTransferable().getTransferData(supp.getDataFlavors()[i]);
        } catch (IOException e) {
          continue;
        } catch (UnsupportedFlavorException e) {
          continue;
        }
      }
    }
    
    return assets;
  }

  public boolean importData(TransferSupport supp) {
    if (!canImport(supp) || !supp.isDrop()) {
        return false;
    }
    
    // Fetch the drop location
    DropLocation loc = supp.getDropLocation();
    CategoryNode dropParent = (CategoryNode) categoryTree.getClosestPathForLocation(loc.getDropPoint().x, loc.getDropPoint().y).getLastPathComponent();
    
    // Try to get the dragged node
    CategoryNode t = getTransferedNode(supp);
    
    if (t != null) {
      // modify category
      t.getCategory().setParent(dropParent.getCategory());
      categoryService.save(t.getCategory());
      // modify nodes
      ((DefaultTreeModel)categoryTree.getModel()).removeNodeFromParent(t);
      insertNodeIntoParent(t, dropParent);
      // set node visible
      categoryTree.expandPath(new TreePath(dropParent.getPath()));
      TreePath newPath = new TreePath(t.getPath());
      categoryTree.setSelectionPath(newPath);
      categoryTree.scrollPathToVisible(newPath);
      
      return true;
    }
    
    List<Asset> assets = getTransferedAssetList(supp);
    if (assets != null && !assets.isEmpty()) {
      for (Iterator<FileImportListener> iter = fileImportListeners.iterator(); iter.hasNext();) {
        iter.next().importAssets(assets, dropParent.getCategory());
      }
      return true;
    }
    
    // Dragged object is not a node: try File list
    List<File> files = getTransferedFiles(supp);
    if (files != null & !files.isEmpty()) {
      // Change selection path:
      categoryTree.setSelectionPath(new TreePath(dropParent.getPath()));
      
      for (Iterator<FileImportListener> iter = fileImportListeners.iterator(); iter.hasNext();) {
        iter.next().importFiles(files, dropParent.getCategory());
      }
      return true;
    }
    
    return false;
  }
  
  /**
   * Inserts {@code node} node as {@code parent} child.
   * <p>
   * The node is inserted so that {@code parent}'s children are sorted lexically.
   * <p>
   * Also ensures that inserted node is visible.
   * 
   * @param node the node to insert
   * @param parent the node in which to insert {@code node}
   */
  @SuppressWarnings("unchecked")
  private void insertNodeIntoParent(CategoryNode node, CategoryNode parent) {
    // Get index of new child node
    int index = -1;
    Enumeration<CategoryNode> children = parent.children();
    while (children.hasMoreElements()) {
      CategoryNode cat = children.nextElement();
      if (!cat.equals(node)) {
        if (cat.getName().compareToIgnoreCase(node.getName()) > 0) {
          break;
        } else {
          index = parent.getIndex(cat);
        }
      }
    }
    if (!children.hasMoreElements()) {
      index++;
    }
    //insert node at index
    ((DefaultTreeModel)categoryTree.getModel()).insertNodeInto(node, parent, index);
    // ensure node is visible
    categoryTree.expandPath(new TreePath(parent.getPath()));
    TreePath newPath = new TreePath(node.getPath());
    categoryTree.setSelectionPath(newPath);
    categoryTree.scrollPathToVisible(newPath);
  }

  /**
   * @return the selectionListener
   */
  public List<CategorySelectionListener> getSelectionListeners() {
    return selectionListeners;
  }

  /**
   * @param selectionListener the selectionListener to set
   */
  public void setSelectionListeners(List<CategorySelectionListener> selectionListeners) {
    this.selectionListeners = selectionListeners;
  }

  /**
   * @return the categoryPopupMenu
   */
  public CategoryPopupMenu getCategoryPopupMenu() {
    return categoryPopupMenu;
  }

  /**
   * @param categoryPopupMenu the categoryPopupMenu to set
   */
  public void setCategoryPopupMenu(CategoryPopupMenu categoryPopupMenu) {
    this.categoryPopupMenu = categoryPopupMenu;
    this.categoryPopupMenu.addActionListener(this);
  }

  /**
   * @return the fileImportListeners
   */
  public List<FileImportListener> getFileImportListeners() {
    return fileImportListeners;
  }

  /**
   * @param fileImportListeners the fileImportListeners to set
   */
  public void setFileImportListeners(List<FileImportListener> fileImportListeners) {
    this.fileImportListeners = fileImportListeners;
  }
}
